msg_tool\scripts\artemis\archive/
pf2.rs1use super::detect_script_type;
3use crate::ext::io::*;
4use crate::scripts::base::*;
5use crate::types::*;
6use crate::utils::struct_pack::*;
7use anyhow::Result;
8use msg_tool_macro::*;
9use std::collections::HashMap;
10use std::io::{Read, Seek, SeekFrom, Write};
11use std::sync::{Arc, Mutex};
12
13#[derive(Debug)]
14pub struct ArtemisPf2Builder {}
16
17impl ArtemisPf2Builder {
18 pub fn new() -> Self {
20 ArtemisPf2Builder {}
21 }
22}
23
24impl ScriptBuilder for ArtemisPf2Builder {
25 fn default_encoding(&self) -> Encoding {
26 Encoding::Cp932
27 }
28
29 fn default_archive_encoding(&self) -> Option<Encoding> {
30 Some(Encoding::Cp932)
31 }
32
33 fn build_script(
34 &self,
35 buf: Vec<u8>,
36 filename: &str,
37 _encoding: Encoding,
38 archive_encoding: Encoding,
39 config: &ExtraConfig,
40 _archive: Option<&Box<dyn Script>>,
41 ) -> Result<Box<dyn Script + Send + Sync>> {
42 Ok(Box::new(ArtemisPf2::new(
43 MemReader::new(buf),
44 archive_encoding,
45 config,
46 filename,
47 )?))
48 }
49
50 fn build_script_from_file(
51 &self,
52 filename: &str,
53 _encoding: Encoding,
54 archive_encoding: Encoding,
55 config: &ExtraConfig,
56 _archive: Option<&Box<dyn Script>>,
57 ) -> Result<Box<dyn Script + Send + Sync>> {
58 let f = std::fs::File::open(filename)?;
59 let f = std::io::BufReader::new(f);
60 Ok(Box::new(ArtemisPf2::new(
61 f,
62 archive_encoding,
63 config,
64 filename,
65 )?))
66 }
67
68 fn build_script_from_reader<'a>(
69 &self,
70 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
71 filename: &str,
72 _encoding: Encoding,
73 archive_encoding: Encoding,
74 config: &ExtraConfig,
75 _archive: Option<&Box<dyn Script>>,
76 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
77 Ok(Box::new(ArtemisPf2::new(
78 reader,
79 archive_encoding,
80 config,
81 filename,
82 )?))
83 }
84
85 fn extensions(&self) -> &'static [&'static str] {
86 gen_artemis_arc_ext!()
87 }
88
89 fn is_archive(&self) -> bool {
90 true
91 }
92
93 fn script_type(&self) -> &'static ScriptType {
94 &ScriptType::ArtemisPf2
95 }
96
97 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
98 if buf_len >= 3 && buf.starts_with(b"pf2") {
99 return Some(20);
100 }
101 None
102 }
103
104 fn create_archive(
105 &self,
106 filename: &str,
107 files: &[&str],
108 encoding: Encoding,
109 _config: &ExtraConfig,
110 ) -> Result<Box<dyn Archive>> {
111 let f = std::fs::File::options()
112 .write(true)
113 .read(true)
114 .create(true)
115 .truncate(true)
116 .open(filename)?;
117 Ok(Box::new(ArtemisPf2Writer::new(f, files, encoding)?))
118 }
119}
120
121#[derive(Debug, Clone, StructPack, StructUnpack)]
122struct Pf2EntryHeader {
123 #[pstring(u32)]
124 name: String,
125 _unk1: u32,
127 _unk2: u32,
128 _unk3: u32,
129 offset: u32,
130 size: u32,
131}
132
133#[derive(Debug)]
134pub struct ArtemisPf2<'a, T: Read + Seek + std::fmt::Debug + Send + Sync + 'a> {
136 reader: Arc<Mutex<T>>,
137 entries: Vec<Pf2EntryHeader>,
138 output_ext: Option<String>,
139 _mark: std::marker::PhantomData<&'a ()>,
140}
141
142impl<'a, T: Read + Seek + std::fmt::Debug + Send + Sync + 'a> ArtemisPf2<'a, T> {
143 pub fn new(
150 mut reader: T,
151 archive_encoding: Encoding,
152 _config: &ExtraConfig,
153 filename: &str,
154 ) -> Result<Self> {
155 let mut magic = [0; 2];
156 reader.read_exact(&mut magic)?;
157 if &magic != b"pf" {
158 return Err(anyhow::anyhow!(
159 "Invalid Artemis PF2 archive magic: {:?}",
160 magic
161 ));
162 }
163 let version = reader.read_u8()?;
164 if version != b'2' {
165 return Err(anyhow::anyhow!(
166 "Unsupported Artemis PF2 archive version: {}",
167 version
168 ));
169 }
170 let _index_size = reader.read_u32()?;
171 let _reserved = reader.read_u32()?;
172 let file_count = reader.read_u32()?;
173 let mut entries = Vec::with_capacity(file_count as usize);
174 for _ in 0..file_count {
175 let header = reader.read_struct(false, archive_encoding, &None)?;
176 entries.push(header);
177 }
178 let output_ext = std::path::Path::new(filename)
179 .extension()
180 .filter(|s| *s != "pfs")
181 .map(|s| s.to_string_lossy().to_string());
182 Ok(ArtemisPf2 {
183 reader: Arc::new(Mutex::new(reader)),
184 entries,
185 output_ext,
186 _mark: std::marker::PhantomData,
187 })
188 }
189}
190
191impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for ArtemisPf2<'b, T> {
192 fn default_output_script_type(&self) -> OutputScriptType {
193 OutputScriptType::Json
194 }
195
196 fn default_format_type(&self) -> FormatOptions {
197 FormatOptions::None
198 }
199
200 fn is_archive(&self) -> bool {
201 true
202 }
203
204 fn iter_archive_filename<'a>(
205 &'a self,
206 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
207 Ok(Box::new(
208 self.entries.iter().map(|header| Ok(header.name.clone())),
209 ))
210 }
211
212 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
213 Ok(Box::new(
214 self.entries.iter().map(|header| Ok(header.offset as u64)),
215 ))
216 }
217
218 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
219 if index >= self.entries.len() {
220 return Err(anyhow::anyhow!(
221 "Index out of bounds: {} (max: {})",
222 index,
223 self.entries.len()
224 ));
225 }
226 let header = &self.entries[index];
227 let mut entry = Pf2Entry {
228 header: header.clone(),
229 reader: self.reader.clone(),
230 pos: 0,
231 script_type: None,
232 };
233 let mut header_buf = [0; 0x20];
234 let readed = entry.read(&mut header_buf)?;
235 entry.pos = 0;
236 entry.script_type = detect_script_type(&header_buf, readed, &entry.header.name);
237 Ok(Box::new(entry))
238 }
239
240 fn archive_output_ext<'a>(&'a self) -> Option<&'a str> {
241 self.output_ext.as_deref()
242 }
243}
244
245#[derive(Debug)]
246struct Pf2Entry<T: Read + Seek + Send + Sync + std::fmt::Debug> {
247 header: Pf2EntryHeader,
248 reader: Arc<Mutex<T>>,
249 pos: u64,
250 script_type: Option<ScriptType>,
251}
252
253impl<T: Read + Seek + Send + Sync + std::fmt::Debug> ArchiveContent for Pf2Entry<T> {
254 fn name(&self) -> &str {
255 &self.header.name
256 }
257
258 fn size(&self) -> Option<u64> {
259 Some(self.header.size as u64)
260 }
261
262 fn script_type(&self) -> Option<&ScriptType> {
263 self.script_type.as_ref()
264 }
265
266 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
267 Ok(Box::new(self))
268 }
269}
270
271impl<T: Read + Seek + Send + Sync + std::fmt::Debug> Read for Pf2Entry<T> {
272 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
273 let mut reader = self.reader.lock().map_err(|e| {
274 std::io::Error::new(
275 std::io::ErrorKind::Other,
276 format!("Failed to lock mutex: {}", e),
277 )
278 })?;
279 reader.seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
280 let remaining = (self.header.size as u64).saturating_sub(self.pos);
281 if remaining == 0 {
282 return Ok(0);
283 }
284 let bytes_to_read = buf.len().min(remaining as usize);
285 let bytes_read = reader.read(&mut buf[..bytes_to_read])?;
286 self.pos += bytes_read as u64;
287 Ok(bytes_read)
288 }
289}
290
291impl<T: Read + Seek + Send + Sync + std::fmt::Debug> Seek for Pf2Entry<T> {
292 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
293 let new_pos = match pos {
294 SeekFrom::Start(offset) => offset,
295 SeekFrom::End(offset) => {
296 if offset < 0 {
297 if (-offset) as u64 > self.header.size as u64 {
298 return Err(std::io::Error::new(
299 std::io::ErrorKind::InvalidInput,
300 "Seek from end exceeds file length",
301 ));
302 }
303 self.header.size as u64 - (-offset) as u64
304 } else {
305 self.header.size as u64 + offset as u64
306 }
307 }
308 SeekFrom::Current(offset) => {
309 if offset < 0 {
310 if (-offset) as u64 > self.pos {
311 return Err(std::io::Error::new(
312 std::io::ErrorKind::InvalidInput,
313 "Seek from current exceeds current position",
314 ));
315 }
316 self.pos.saturating_sub((-offset) as u64)
317 } else {
318 self.pos + offset as u64
319 }
320 }
321 };
322 self.pos = new_pos;
323 Ok(self.pos)
324 }
325
326 fn stream_position(&mut self) -> std::io::Result<u64> {
327 Ok(self.pos)
328 }
329}
330
331pub struct ArtemisPf2Writer<T: Write + Seek + Read> {
333 writer: T,
334 headers: HashMap<String, Pf2EntryHeader>,
335 encoding: Encoding,
336 index_size: u32,
337}
338
339impl<T: Write + Seek + Read> ArtemisPf2Writer<T> {
340 pub fn new(mut writer: T, files: &[&str], encoding: Encoding) -> Result<Self> {
346 writer.write_all(b"pf2")?;
347 writer.write_u32(0)?; writer.write_u32(0)?; writer.write_u32(files.len() as u32)?;
350 let mut headers = HashMap::new();
351 for file in files {
352 let header = Pf2EntryHeader {
353 name: file.to_string(),
354 _unk1: 0x10,
355 _unk2: 0,
356 _unk3: 0,
357 offset: 0,
358 size: 0,
359 };
360 header.pack(&mut writer, false, encoding, &None)?;
361 headers.insert(file.to_string(), header);
362 }
363 let size = writer.stream_position()?;
364 let index_size = size as u32 - 7;
365 writer.write_u32_at(3, index_size)?;
366 writer.write_u32_at(7, 0)?;
367 Ok(ArtemisPf2Writer {
368 writer,
369 headers,
370 encoding,
371 index_size,
372 })
373 }
374}
375
376impl<T: Write + Seek + Read> Archive for ArtemisPf2Writer<T> {
377 fn new_file<'a>(
378 &'a mut self,
379 name: &str,
380 _size: Option<u64>,
381 ) -> Result<Box<dyn WriteSeek + 'a>> {
382 let entry = self
383 .headers
384 .get_mut(name)
385 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
386 if entry.offset != 0 || entry.size != 0 {
387 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
388 }
389 self.writer.seek(SeekFrom::End(0))?;
390 entry.offset = self.writer.stream_position()? as u32;
391 let file = ArtemisPf2File {
392 header: entry,
393 writer: &mut self.writer,
394 pos: 0,
395 };
396 Ok(Box::new(file))
397 }
398
399 fn write_header(&mut self) -> Result<()> {
400 self.writer.seek(SeekFrom::Start(15))?;
401 let mut files = self.headers.values().collect::<Vec<_>>();
402 files.sort_by_key(|d| d.offset);
403 for file in files.iter() {
404 file.pack(&mut self.writer, false, self.encoding, &None)?;
405 }
406 self.writer.write_u32_at(3, self.index_size)?;
407 self.writer.write_u32_at(7, 0)?;
408 Ok(())
409 }
410}
411
412pub struct ArtemisPf2File<'a, T: Write + Seek> {
414 header: &'a mut Pf2EntryHeader,
415 writer: &'a mut T,
416 pos: u64,
417}
418
419impl<'a, T: Write + Seek> Write for ArtemisPf2File<'a, T> {
420 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
421 self.writer
422 .seek(SeekFrom::Start(self.header.offset as u64 + self.pos))?;
423 let bytes_written = self.writer.write(buf)?;
424 self.pos += bytes_written as u64;
425 self.header.size = self.header.size.max(self.pos as u32);
426 Ok(bytes_written)
427 }
428
429 fn flush(&mut self) -> std::io::Result<()> {
430 self.writer.flush()
431 }
432}
433
434impl<'a, T: Write + Seek> Seek for ArtemisPf2File<'a, T> {
435 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
436 let new_pos = match pos {
437 SeekFrom::Start(offset) => offset,
438 SeekFrom::End(offset) => {
439 if offset < 0 {
440 if (-offset) as u64 > self.header.size as u64 {
441 return Err(std::io::Error::new(
442 std::io::ErrorKind::InvalidInput,
443 "Seek from end exceeds file length",
444 ));
445 }
446 self.header.size as u64 - (-offset) as u64
447 } else {
448 self.header.size as u64 + offset as u64
449 }
450 }
451 SeekFrom::Current(offset) => {
452 if offset < 0 {
453 if (-offset) as u64 > self.pos {
454 return Err(std::io::Error::new(
455 std::io::ErrorKind::InvalidInput,
456 "Seek from current exceeds current position",
457 ));
458 }
459 self.pos.saturating_sub((-offset) as u64)
460 } else {
461 self.pos + offset as u64
462 }
463 }
464 };
465 self.pos = new_pos;
466 Ok(self.pos)
467 }
468
469 fn stream_position(&mut self) -> std::io::Result<u64> {
470 Ok(self.pos)
471 }
472}